/**
 * @file        lcd.c
 * @brief       LCD驱动实现文件
 * @details     本驱动支持多种TFT LCD控制器的自动识别、初始化和显示操作
 *              
 *              支持的LCD控制器：
 *              - ILI9341: 240×320分辨率，广泛使用的TFT控制器
 *              - ST7789:  240×320分辨率，新一代高性能控制器
 *              - NT35310: 320×480分辨率，中等尺寸屏幕
 *              - ST7796:  320×480分辨率，高色彩还原度
 *              - NT35510: 480×800分辨率，大尺寸高分辨率
 *              - ILI9806: 480×800分辨率，高性能大屏控制器
 *              - SSD1963: 800×480分辨率，超大屏幕专用控制器
 */

#include "stdlib.h"
#include "stdio.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/LCD/lcdfont.h"
 
/* lcd_ex.c存放各个LCD驱动IC的寄存器初始化部分代码,以简化lcd.c,该.c文件
 * 不直接加入到工程里面,只有lcd.c会用到,所以通过include的形式添加.(不要在
 * 其他文件再包含该.c文件!!否则会报错!)
 */
#include "./BSP/LCD/lcd_ex.c"

/* -------------------------------------------- 全局变量定义 ------------------------------------------- */

/* LCD的画笔颜色和背景色 */
uint32_t g_point_color = BLUE;      /* 画笔颜色 */
uint32_t g_back_color  = WHITE;     /* 背景色 */

/* 管理LCD重要参数 */
_lcd_dev lcddev;

/* -------------------------------------------- 基础操作函数 ------------------------------------------- */

/**
 * @brief       向LCD数据寄存器写入16位数据
 * @param       data: 要写入的16位数据值
 * @retval      无
 * @note        data参数使用volatile修饰，防止编译器优化
 *              在使用-O2优化时，需要通过赋值操作插入必要的延时
 */
void lcd_wr_data(volatile uint16_t data)
{
    LCD_RS(1);
    LCD_CS(0);
    LCD_DATA_OUT(data);
    LCD_WR(0);
    LCD_WR(1);
    LCD_CS(1);
}

/**
 * @brief       向LCD命令寄存器写入寄存器地址
 * @param       regno: 目标寄存器的地址/编号（16位）
 * @retval      无
 * @note        此函数用于选择要操作的LCD内部寄存器
 *              regno参数使用volatile修饰，防止编译器优化
 *              在使用-O2优化时，需要通过赋值操作插入必要的延时
 */
void lcd_wr_regno(volatile uint16_t regno)
{
    LCD_RS(0);              /* RS=0,表示写寄存器 */
    LCD_CS(0);
    LCD_DATA_OUT(regno);    /* 写入要写的寄存器序号 */
    LCD_WR(0);
    LCD_WR(1);
    LCD_CS(1);
}

/**
 * @brief       向LCD指定寄存器写入数据（组合操作）
 * @param       regno: 目标寄存器地址/编号（16位）
 * @param       data:  要写入寄存器的数据值（16位）
 * @retval      无
 * @note        此函数是lcd_wr_regno()和lcd_wr_data()的组合操作
 *              先选择寄存器，再写入数据，适用于单次寄存器配置
 */
void lcd_write_reg(uint16_t regno, uint16_t data)
{
    lcd_wr_regno(regno);    /* 写入要写的寄存器序号 */
    lcd_wr_data(data);      /* 写入数据 */
}

/**
 * @brief       LCD操作延时函数
 * @param       i: 延时循环次数（32位无符号整数）
 * @retval      无
 * @note        此函数专门用于LCD时序要求的延时操作
 *              在MDK -O1及以上优化等级时，某些LCD操作需要插入延时
 *              使用AC6编译器时，空循环可能被优化掉，可改用内联汇编
 *              替代方案：while(1) __asm volatile("");
 */
static void lcd_opt_delay(uint32_t i)
{
    while (i--); /* 简单延时循环，防止编译器过度优化影响LCD时序 */
}

/**
 * @brief       从LCD数据寄存器读取16位数据
 * @param       无
 * @retval      从LCD读取到的16位数据值
 * @note        使用volatile修饰返回值变量，防止编译器优化
 *              读取前需要适当延时，确保LCD数据准备就绪
 *              主要用于读取像素颜色值等操作
 */
static uint16_t lcd_rd_data(void)
{
    volatile uint16_t ram;  /* 使用volatile防止编译器优化读取操作 */
    
    GPIO_InitTypeDef gpio_init_struct;
    /* LCD_DATA 引脚模式设置, 上拉输入, 准备接收数据 */
    gpio_init_struct.Pin = LCD_DATA_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_INPUT;
    gpio_init_struct.Pull = GPIO_PULLUP;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(LCD_DATA_GPIO_PORT, &gpio_init_struct); 

    LCD_RS(1);              /* RS=1,表示操作数据 */
    LCD_CS(0);
    LCD_RD(0);
    lcd_opt_delay(2);
    ram = LCD_DATA_IN;      /* 读取数据 */
    LCD_RD(1);
    LCD_CS(1);
    
    /* LCD_DATA 引脚模式设置, 推挽输出, 恢复输出状态 */
    gpio_init_struct.Pin = LCD_DATA_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_init_struct.Pull = GPIO_PULLUP;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(LCD_DATA_GPIO_PORT, &gpio_init_struct);
    return ram;
}

/**
 * @brief       准备写入LCD显存（GRAM）
 * @param       无
 * @retval      无
 * @note        向LCD发送写GRAM命令，准备后续的像素数据写入
 *              不同LCD控制器的写GRAM命令可能不同，通过lcddev.wramcmd统一管理
 *              调用此函数后，可连续写入像素数据到LCD->LCD_RAM
 */
void lcd_write_ram_prepare(void)
{
    lcd_wr_regno(lcddev.wramcmd);  /* 发送写GRAM命令到LCD命令寄存器 */
}

/**
 * @brief       读取LCD指定坐标像素点的颜色值
 * @param       x: 像素点的X坐标（0 ~ lcddev.width-1）
 * @param       y: 像素点的Y坐标（0 ~ lcddev.height-1）
 * @retval      像素点的颜色值（32位，兼容LTDC，低16位为有效数据）
 * @note        此函数针对不同LCD控制器采用不同的读取策略：
 *              - NT35510: 使用特殊的读取命令 0x2E00
 *              - SSD1963: 直接读取，无需dummy read
 *              - ST7796: 一次读取完整像素值
 *              - 其他控制器: 需要dummy read + 实际读取
 *              读取过程：设置坐标 -> 发送读取命令 -> 读取数据
 *              返回的颜色值格式通常为RGB565或RGB888
 * @warning     读取操作比写入操作慢，不建议频繁使用
 *              坐标超出边界时返回0，调用前应检查坐标有效性
 *              不同控制器的读取时序和数据格式可能不同
 */
uint32_t lcd_read_point(uint16_t x, uint16_t y)
{
    uint16_t r = 0, g = 0, b = 0;

    if (x >= lcddev.width || y >= lcddev.height)
    {
        return 0;   /* 超过了范围,直接返回 */
    }

    lcd_set_cursor(x, y);       /* 设置坐标 */

    if (lcddev.id == 0x5510)
    {
        lcd_wr_regno(0x2E00);   /* 5510 发送读GRAM指令 */
    }
    else
    {
        lcd_wr_regno(0x2E);     /* 9341/5310/1963/7789/7796/9806 等发送读GRAM指令 */
    }


    r = lcd_rd_data();          /* 假读(dummy read) */

    if (lcddev.id == 0x1963)
    {
        return r;   /* 1963直接读就可以 */
    }

    r = lcd_rd_data();          /* 实际坐标颜色 */
    
    if (lcddev.id == 0x7796)    /* 7796 一次读取一个像素值 */
    {
        return r;
    }
    
    /* 9341/5310/5510/7789/9806要分2次读出 */
    b = lcd_rd_data();
    g = r & 0xFF;               /* 对于9341/5310/5510/7789/9806,第一次读取的是RG的值,R在前,G在后,各占8位 */
    g <<= 8;
    
    return (((r >> 11) << 11) | ((g >> 10) << 5) | (b >> 11));  /* ILI9341/NT35310/NT35510/ST7789/ILI9806需要公式转换一下 */
}

/* -------------------------------------------- 初始化和控制函数 ------------------------------------------- */

/**
 * @brief       LCD开启显示
 * @param       无
 * @retval      无
 */
void lcd_display_on(void)
{
    if (lcddev.id == 0x5510)
    {
        lcd_wr_regno(0x2900);   /* 开启显示 */
    }
    else                        /* 9341/5310/1963/7789/7796/9806 等发送开启显示指令 */
    {
        lcd_wr_regno(0x29);     /* 开启显示 */
    }
}

/**
 * @brief       LCD关闭显示
 * @param       无
 * @retval      无
 */
void lcd_display_off(void)
{
    if (lcddev.id == 0x5510)
    {
        lcd_wr_regno(0x2800);   /* 关闭显示 */
    }
    else                        /* 9341/5310/1963/7789/7796/9806 等发送关闭显示指令 */
    {
        lcd_wr_regno(0x28);     /* 关闭显示 */
    }
}

/**
 * @brief       设置LCD光标到指定像素位置（不RGB并行接口屏幕无效）
 * @param       x: 目标像素的X坐标（0 ~ lcddev.width-1）
 * @param       y: 目标像素的Y坐标（0 ~ lcddev.height-1）
 * @retval      无
 * @note        此函数针对不同LCD控制器采用不同的坐标设置策略：
 *              - SSD1963: 需要根据显示方向进行坐标变换
 *                竖屏模式: X坐标需要反向映射 (width-1-x)
 *                横屏模式: 直接设置X坐标范围
 *              - 其他控制器: 直接设置X/Y坐标寄存器
 *              光标设置后，后续的像素数据写入将从此位置开始
 *              使用lcddev.setxcmd和lcddev.setycmd统一管理不同控制器的命令
 * @warning     RGB并行接口屏幕（如LTDC驱动）不需要此操作
 *              不检查坐标边界，调用前需确保坐标在有效范围内
 *              SSD1963的坐标变换逻辑较为复杂，需要根据方向调整
 */
void lcd_set_cursor(uint16_t x, uint16_t y)
{
    if (lcddev.id == 0x1963)
    {
        if (lcddev.dir == 0)    /* 竖屏模式, x坐标需要变换 */
        {
            x = lcddev.width - 1 - x;
            lcd_wr_regno(lcddev.setxcmd);
            lcd_wr_data(0);
            lcd_wr_data(0);
            lcd_wr_data(x >> 8);
            lcd_wr_data(x & 0xFF);
        }
        else                    /* 横屏模式 */
        {
            lcd_wr_regno(lcddev.setxcmd);
            lcd_wr_data(x >> 8);
            lcd_wr_data(x & 0xFF);
            lcd_wr_data((lcddev.width - 1) >> 8);
            lcd_wr_data((lcddev.width - 1) & 0xFF);
        }

        lcd_wr_regno(lcddev.setycmd);
        lcd_wr_data(y >> 8);
        lcd_wr_data(y & 0xFF);
        lcd_wr_data((lcddev.height - 1) >> 8);
        lcd_wr_data((lcddev.height - 1) & 0xFF);

    }
    else if (lcddev.id == 0x5510)
    {
        lcd_wr_regno(lcddev.setxcmd);
        lcd_wr_data(x >> 8);
        lcd_wr_regno(lcddev.setxcmd + 1);
        lcd_wr_data(x & 0xFF);
        lcd_wr_regno(lcddev.setycmd);
        lcd_wr_data(y >> 8);
        lcd_wr_regno(lcddev.setycmd + 1);
        lcd_wr_data(y & 0xFF);
    }
    else    /* 9341/5310/7789/7796/9806 等 设置坐标 */
    {
        lcd_wr_regno(lcddev.setxcmd);
        lcd_wr_data(x >> 8);
        lcd_wr_data(x & 0xFF);
        lcd_wr_regno(lcddev.setycmd);
        lcd_wr_data(y >> 8);
        lcd_wr_data(y & 0xFF);
    }
}

/**
 * @brief       设置LCD的自动扫描方向（对RGB并行接口屏幕无效）
 * @param       dir: 扫描方向选择（0~7，代表8种不同的扫描方向）
 *                   具体定义参见lcd.h文件中的宏定义：
 *                   0(L2R_U2D) - 从左到右，从上到下（推荐默认值）
 *                   1~7 - 其他组合的扫描方向
 * @retval      无
 * @note        此函数控制LCD内部GRAM的读写方向和顺序
 *              已经在以下LCD控制器上实际测试通过：
 *              ILI9341/NT35310/NT35510/SSD1963/ST7789/ST7796/ILI9806
 *              
 *              重要特殊处理：
 *              - SSD1963: 横屏时不改变扫描方向，竖屏时才改变
 *              - 其他控制器: 横屏时需要进行方向转换
 *              
 *              方向转换逻辑（针对横屏模式）：
 *              0->6, 1->7, 2->4, 3->5, 4->1, 5->0, 6->3, 7->2
 * @warning     此函数设置会影响其他绘图函数的显示效果（尤其是ILI9341）
 *              建议使用默认值 L2R_U2D(0)，其他设置可能导致显示异常
 *              RGB并行接口屏幕（如LTDC驱动）不需要此操作
 *              不同控制器的扫描方向寄存器地址和位定义可能不同
 */
void lcd_scan_dir(uint8_t dir)
{
    uint16_t regval = 0;
    uint16_t dirreg = 0;
    uint16_t temp;

    /* 横屏时，对1963不改变扫描方向！竖屏时1963改变方向(这里仅用于1963的特殊处理,对其他驱动IC无效) */
    if ((lcddev.dir == 1 && lcddev.id != 0x1963) || (lcddev.dir == 0 && lcddev.id == 0x1963))
    {
        switch (dir)   /* 方向转换 */
        {
            case 0:
                dir = 6;
                break;

            case 1:
                dir = 7;
                break;

            case 2:
                dir = 4;
                break;

            case 3:
                dir = 5;
                break;

            case 4:
                dir = 1;
                break;

            case 5:
                dir = 0;
                break;

            case 6:
                dir = 3;
                break;

            case 7:
                dir = 2;
                break;
        }
    }


    /* 根据扫描方式 设置 0x36/0x3600 寄存器 bit 5,6,7 位的值 */
    switch (dir)
    {
        case L2R_U2D:   /* 从左到右,从上到下 */
            regval |= (0 << 7) | (0 << 6) | (0 << 5);
            break;

        case L2R_D2U:   /* 从左到右,从下到上 */
            regval |= (1 << 7) | (0 << 6) | (0 << 5);
            break;

        case R2L_U2D:   /* 从右到左,从上到下 */
            regval |= (0 << 7) | (1 << 6) | (0 << 5);
            break;

        case R2L_D2U:   /* 从右到左,从下到上 */
            regval |= (1 << 7) | (1 << 6) | (0 << 5);
            break;

        case U2D_L2R:   /* 从上到下,从左到右 */
            regval |= (0 << 7) | (0 << 6) | (1 << 5);
            break;

        case U2D_R2L:   /* 从上到下,从右到左 */
            regval |= (0 << 7) | (1 << 6) | (1 << 5);
            break;

        case D2U_L2R:   /* 从下到上,从左到右 */
            regval |= (1 << 7) | (0 << 6) | (1 << 5);
            break;

        case D2U_R2L:   /* 从下到上,从右到左 */
            regval |= (1 << 7) | (1 << 6) | (1 << 5);
            break;
    }

    dirreg = 0x36;  /* 对绝大部分驱动IC, 由0x36寄存器控制 */

    if (lcddev.id == 0x5510)
    {
        dirreg = 0x3600;    /* 对于5510, 和其他驱动ic的寄存器有差异 */
    }

     /* 9341 & 7789 & 7796 要设置BGR位 */
    if (lcddev.id == 0x9341 || lcddev.id == 0x7789 || lcddev.id == 0x7796)
    {
        regval |= 0x08;
    }

    lcd_write_reg(dirreg, regval);

    if (lcddev.id != 0x1963)                    /* 1963不做坐标处理 */
    {
        if (regval & 0x20)
        {
            if (lcddev.width < lcddev.height)   /* 交换X,Y */
            {
                temp = lcddev.width;
                lcddev.width = lcddev.height;
                lcddev.height = temp;
            }
        }
        else
        {
            if (lcddev.width > lcddev.height)   /* 交换X,Y */
            {
                temp = lcddev.width;
                lcddev.width = lcddev.height;
                lcddev.height = temp;
            }
        }
    }

    /* 设置显示区域(开窗)大小 */
    if (lcddev.id == 0x5510)
    {
        lcd_wr_regno(lcddev.setxcmd);
        lcd_wr_data(0);
        lcd_wr_regno(lcddev.setxcmd + 1);
        lcd_wr_data(0);
        lcd_wr_regno(lcddev.setxcmd + 2);
        lcd_wr_data((lcddev.width - 1) >> 8);
        lcd_wr_regno(lcddev.setxcmd + 3);
        lcd_wr_data((lcddev.width - 1) & 0xFF);
        lcd_wr_regno(lcddev.setycmd);
        lcd_wr_data(0);
        lcd_wr_regno(lcddev.setycmd + 1);
        lcd_wr_data(0);
        lcd_wr_regno(lcddev.setycmd + 2);
        lcd_wr_data((lcddev.height - 1) >> 8);
        lcd_wr_regno(lcddev.setycmd + 3);
        lcd_wr_data((lcddev.height - 1) & 0xFF);
    }
    else
    {
        lcd_wr_regno(lcddev.setxcmd);
        lcd_wr_data(0);
        lcd_wr_data(0);
        lcd_wr_data((lcddev.width - 1) >> 8);
        lcd_wr_data((lcddev.width - 1) & 0xFF);
        lcd_wr_regno(lcddev.setycmd);
        lcd_wr_data(0);
        lcd_wr_data(0);
        lcd_wr_data((lcddev.height - 1) >> 8);
        lcd_wr_data((lcddev.height - 1) & 0xFF);
    }
}

/* -------------------------------------------- 基本绘图函数 ------------------------------------------- */

/**
 * @brief       在LCD指定坐标绘制单个像素点
 * @param       x: 像素点的X坐标（0 ~ lcddev.width-1）
 * @param       y: 像素点的Y坐标（0 ~ lcddev.height-1）
 * @param       color: 像素点颜色值（32位，兼容LTDC，实际使用低16位）
 * @retval      无
 * @note        这是LCD绘图的最基础函数，其他绘图函数都基于此函数实现
 *              坐标系原点(0,0)位于屏幕左上角
 *              颜色格式通常为RGB565（16位）或RGB888（24位）
 * @warning     不检查坐标边界，调用前需确保坐标在有效范围内
 */
void lcd_draw_point(uint16_t x, uint16_t y, uint32_t color)
{
    lcd_set_cursor(x, y);       /* 设置LCD光标到目标像素位置 */
    lcd_write_ram_prepare();    /* 准备写入显存，发送写GRAM命令 */
    lcd_wr_data(color);         /* 写入像素颜色数据到显存 */
}

/**
 * @brief       设置SSD1963控制器的背光亮度
 * @param       pwm: 背光亮度等级（0~100），0为最暗，100为最亮
 * @retval      无
 * @note        此函数专门用于SSD1963控制器的背光控制
 *              通过配置SSD1963内部PWM模块来调节背光亮度
 *              PWM占空比 = pwm * 2.55，将0~100映射到0~255
 *              其他LCD控制器可能不支持此功能或使用不同的控制方式
 * @warning     仅适用于使用SSD1963控制器的LCD屏幕
 */
void lcd_ssd_backlight_set(uint8_t pwm)
{
    lcd_wr_regno(0xBE);         /* 选择SSD1963的PWM配置寄存器 */
    lcd_wr_data(0x05);          /* 参数1：设置PWM频率分频系数 */
    lcd_wr_data(pwm * 2.55);    /* 参数2：设置PWM占空比（0~255） */
    lcd_wr_data(0x01);          /* 参数3：PWM控制参数C */
    lcd_wr_data(0xFF);          /* 参数4：PWM控制参数D */
    lcd_wr_data(0x00);          /* 参数5：PWM控制参数E */
    lcd_wr_data(0x00);          /* 参数6：PWM控制参数F */
}

/**
 * @brief       设置LCD显示方向（横屏/竖屏切换）
 * @param       dir: 显示方向选择
 *                   0 - 竖屏模式（Portrait）
 *                   1 - 横屏模式（Landscape）
 * @retval      无
 * @note        此函数会重新配置LCD的显示参数：
 *              - 更新屏幕宽度和高度（lcddev.width/height）
 *              - 重新设置写GRAM命令（lcddev.wramcmd）
 *              - 重新设置坐标设置命令（lcddev.setxcmd/setycmd）
 *              不同的LCD控制器支持的分辨率和命令可能不同
 * @warning     切换显示方向后，之前绘制的内容可能会错乱
 *              建议在初始化时设置，避免运行时频繁切换
 */
void lcd_display_dir(uint8_t dir)
{
    lcddev.dir = dir;   /* 竖屏/横屏 */

    if (dir == 0)       /* 竖屏 */
    {
        lcddev.width = 240;
        lcddev.height = 320;

        if (lcddev.id == 0x5510)
        {
            lcddev.wramcmd = 0x2C00;
            lcddev.setxcmd = 0x2A00;
            lcddev.setycmd = 0x2B00;
            lcddev.width = 480;
            lcddev.height = 800;
        }
        else if (lcddev.id == 0x1963)
        {
            lcddev.wramcmd = 0x2C;  /* 设置写入GRAM的指令 */
            lcddev.setxcmd = 0x2B;  /* 设置写X坐标指令 */
            lcddev.setycmd = 0x2A;  /* 设置写Y坐标指令 */
            lcddev.width = 480;     /* 设置宽度480 */
            lcddev.height = 800;    /* 设置高度800 */
        }
        else   /* 其他IC, 包括: 9341/5310/7789/7796/9806等IC */
        {
            lcddev.wramcmd = 0x2C;
            lcddev.setxcmd = 0x2A;
            lcddev.setycmd = 0x2B;
        }

        if (lcddev.id == 0x5310 || lcddev.id == 0x7796)     /* 如果是5310/7796 则表示是 320*480分辨率 */
        {
            lcddev.width = 320;
            lcddev.height = 480;
        }
        
        if (lcddev.id == 0X9806)    /* 如果是9806 则表示是 480*800 分辨率 */
        {
            lcddev.width = 480;
            lcddev.height = 800;
        }  
    }
    else        /* 横屏 */
    {
        lcddev.width = 320;         /* 默认宽度 */
        lcddev.height = 240;        /* 默认高度 */

        if (lcddev.id == 0x5510)
        {
            lcddev.wramcmd = 0x2C00;
            lcddev.setxcmd = 0x2A00;
            lcddev.setycmd = 0x2B00;
            lcddev.width = 800;
            lcddev.height = 480;
        }
        else if (lcddev.id == 0x1963 || lcddev.id == 0x9806)
        {
            lcddev.wramcmd = 0x2C;  /* 设置写入GRAM的指令 */
            lcddev.setxcmd = 0x2A;  /* 设置写X坐标指令 */
            lcddev.setycmd = 0x2B;  /* 设置写Y坐标指令 */
            lcddev.width = 800;     /* 设置宽度800 */
            lcddev.height = 480;    /* 设置高度480 */
        }
        else   /* 其他IC, 包括:9341/5310/7789/7796等IC */
        {
            lcddev.wramcmd = 0x2C;
            lcddev.setxcmd = 0x2A;
            lcddev.setycmd = 0x2B;
        }

        if (lcddev.id == 0x5310 || lcddev.id == 0x7796)     /* 如果是5310/7796 则表示是 320*480分辨率 */
        {
            lcddev.width = 480;
            lcddev.height = 320;
        }
    }

    lcd_scan_dir(DFT_SCAN_DIR);     /* 默认扫描方向 */
}

/**
 * @brief       设置LCD显示方向（支持4种显示方向）
 * @param       direction: 显示方向
 *                   0 - 竖屏正向（Portrait Normal）
 *                   1 - 横屏正向（Landscape Normal）
 *                   2 - 竖屏反向（Portrait Reversed）
 *                   3 - 横屏反向（Landscape Reversed）
 * @retval      无
 * @note        此函数通过组合显示方向和扫描方向来实现4个显示方向
 *              - 竖屏正向: 竖屏 + L2R_U2D（正常竖屏）
 *              - 横屏正向: 横屏 + L2R_U2D（正常横屏）
 *              - 竖屏反向: 竖屏 + R2L_D2U（反向竖屏）
 *              - 横屏反向: 横屏 + R2L_D2U（反向横屏）
 */
void lcd_set_direction(uint8_t direction)
{
    switch (direction)
    {
        case 0:     /* 竖屏正向 */
            lcd_display_dir(0);         /* 竖屏模式 */
            lcd_scan_dir(L2R_U2D);      /* 从左到右，从上到下 */
            break;
            
        case 1:     /* 横屏正向 */
            lcd_display_dir(1);         /* 横屏模式 */
            lcd_scan_dir(L2R_U2D);      /* 横屏正常扫描 */
            break;
            
        case 2:     /* 竖屏反向 */
            lcd_display_dir(0);         /* 竖屏模式 */
            lcd_scan_dir(R2L_D2U);      /* 从右到左，从下到上（反向） */
            break;
            
        case 3:     /* 横屏反向 */
            lcd_display_dir(1);         /* 横屏模式 */
            lcd_scan_dir(R2L_D2U);      /* 横屏反向扫描 */
            break;
            
        default:    /* 默认为竖屏正向 */
            lcd_display_dir(0);
            lcd_scan_dir(L2R_U2D);
            break;
    }
}

/**
 * @brief       设置LCD绘图窗口区域（对RGB并行接口屏幕无效）
 * @param       sx: 窗口区域的左上角X坐标
 * @param       sy: 窗口区域的左上角Y坐标
 * @param       width: 窗口宽度（像素单位，必须 > 0）
 * @param       height: 窗口高度（像素单位，必须 > 0）
 * @retval      无
 * @note        此函数设置一个矩形窗口区域，后续的像素数据写入只在此区域内有效
 *              窗口大小 = width * height 个像素
 *              设置后会自动将光标位置设置到窗口左上角(sx,sy)
 *              
 *              不同LCD控制器的特殊处理：
 *              - SSD1963竖屏模式: 需要进行坐标变换和特殊处理
 *              - 其他控制器: 直接设置X/Y坐标范围
 *              
 *              使用场景：
 *              - 部分区域更新（提高效率）
 *              - 图像显示到指定区域
 *              - 窗口内的连续像素操作
 * @warning     RGB并行接口屏幕（如LTDC驱动）不需要此操作
 *              width和height必须大于0，否则可能导致不可预料的结果
 *              窗口范围不能超出屏幕边界，调用前需检查参数有效性
 *              SSD1963在竖屏模式下的坐标变换逻辑较为复杂
 */
void lcd_set_window(uint16_t sx, uint16_t sy, uint16_t width, uint16_t height)
{
    uint16_t twidth, theight;
    twidth = sx + width - 1;
    theight = sy + height - 1;

   
   if (lcddev.id == 0x1963 && lcddev.dir != 1)     /* 1963竖屏特殊处理 */
    {
        sx = lcddev.width - width - sx;
        height = sy + height - 1;
        lcd_wr_regno(lcddev.setxcmd);
        lcd_wr_data(sx >> 8);
        lcd_wr_data(sx & 0xFF);
        lcd_wr_data((sx + width - 1) >> 8);
        lcd_wr_data((sx + width - 1) & 0xFF);
        lcd_wr_regno(lcddev.setycmd);
        lcd_wr_data(sy >> 8);
        lcd_wr_data(sy & 0xFF);
        lcd_wr_data(height >> 8);
        lcd_wr_data(height & 0xFF);
    }
    else if (lcddev.id == 0x5510)
    {
        lcd_wr_regno(lcddev.setxcmd);
        lcd_wr_data(sx >> 8);
        lcd_wr_regno(lcddev.setxcmd + 1);
        lcd_wr_data(sx & 0xFF);
        lcd_wr_regno(lcddev.setxcmd + 2);
        lcd_wr_data(twidth >> 8);
        lcd_wr_regno(lcddev.setxcmd + 3);
        lcd_wr_data(twidth & 0xFF);
        lcd_wr_regno(lcddev.setycmd);
        lcd_wr_data(sy >> 8);
        lcd_wr_regno(lcddev.setycmd + 1);
        lcd_wr_data(sy & 0xFF);
        lcd_wr_regno(lcddev.setycmd + 2);
        lcd_wr_data(theight >> 8);
        lcd_wr_regno(lcddev.setycmd + 3);
        lcd_wr_data(theight & 0xFF);
    }
    else    /* 9341/5310/7789/1963/7796/9806横屏 等 设置窗口 */
    {
        lcd_wr_regno(lcddev.setxcmd);
        lcd_wr_data(sx >> 8);
        lcd_wr_data(sx & 0xFF);
        lcd_wr_data(twidth >> 8);
        lcd_wr_data(twidth & 0xFF);
        lcd_wr_regno(lcddev.setycmd);
        lcd_wr_data(sy >> 8);
        lcd_wr_data(sy & 0xFF);
        lcd_wr_data(theight >> 8);
        lcd_wr_data(theight & 0xFF);
    }
}

/**
 * @brief       LCD驱动初始化函数 - 支持多种LCD控制器自动识别与配置
 * @param       无
 * @retval      无
 * @note        这是LCD驱动的核心初始化函数，执行以下关键步骤：
 *              
 *              1. GPIO硬件接口初始化：
 *                 - 配置控制引脚（CS/WR/RD/RS/BL）为推挽输出模式
 *                 - 配置16位数据引脚（GPIOB全部引脚）为推挽输出
 *                 - 禁用JTAG功能，释放PB3/PB4引脚用作普通GPIO
 *                 - 设置引脚默认电平状态
 *              
 *              2. LCD控制器自动识别：
 *                 支持的控制器类型及ID读取方法：
 *                 - ILI9341: 读取0xD3寄存器 → ID=0x9341
 *                 - ST7789:  读取0x04寄存器 → ID=0x8552→转换为0x7789
 *                 - NT35310: 读取0xD4寄存器 → ID=0x5310
 *                 - ST7796:  读取0xD3寄存器 → ID=0x7796
 *                 - NT35510: 需要密钥解锁后读取0xC500/0xC501 → ID=0x5510
 *                 - ILI9806: 读取0xD3寄存器 → ID=0x9806
 *                 - SSD1963: 读取0xA1寄存器 → ID=0x5761→转换为0x1963
 *              
 *              3. 控制器特定初始化：
 *                 - 根据识别的控制器ID调用对应的寄存器初始化函数
 *                 - 配置显示参数、伽马校正、电源管理等
 *                 - SSD1963额外设置背光亮度为100%
 *              
 *              4. GPIO时序特性：
 *                 - 使用GPIO模拟8080并行时序
 *                 - 通过软件延时和volatile关键字确保时序正确
 *                 - 不同控制器可能需要不同的延时参数
 *              
 *              5. 默认设置：
 *                 - 设置为竖屏显示方向
 *                 - 点亮背光
 *                 - 清屏为白色
 * 
 * @warning     初始化过程中的关键注意事项：
 *              - 必须先初始化串口1，否则printf会导致系统卡死
 *              - GPIO时序对显示稳定性至关重要，需根据硬件调整
 *              - 长排线连接会影响时序，可能需要增加延时
 *              - NT35510需要厂家提供的密钥序列才能读取ID
 *              - 某些控制器的读操作需要dummy read来获取正确数据
 *              
 * @example     典型使用流程：
 *              ```c
 *              // 在main函数中调用
 *              lcd_init();                    // 自动识别并初始化LCD
 *              printf("LCD ID: %x\n", lcddev.id); // 显示识别的控制器ID
 *              lcd_show_string(0,0,200,16,16,"Hello LCD!",RED); // 显示文本
 *              ```
 */
void lcd_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;

    LCD_CS_GPIO_CLK_ENABLE();   /* LCD_CS脚时钟使能 */
    LCD_WR_GPIO_CLK_ENABLE();   /* LCD_WR脚时钟使能 */
    LCD_RD_GPIO_CLK_ENABLE();   /* LCD_RD脚时钟使能 */
    LCD_RS_GPIO_CLK_ENABLE();   /* LCD_RS脚时钟使能 */
    LCD_BL_GPIO_CLK_ENABLE();   /* LCD_BL脚时钟使能 */
    LCD_DATA_GPIO_CLK_ENABLE(); /* LCD_DATA脚时钟使能 */
    
    __HAL_RCC_AFIO_CLK_ENABLE();
    __HAL_AFIO_REMAP_SWJ_NOJTAG(); /* 禁止JTAG, 使能SWD, 释放PB3,PB4两个引脚做普通IO用 */
    
    gpio_init_struct.Pin = LCD_CS_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;          /* 高速 */
    HAL_GPIO_Init(LCD_CS_GPIO_PORT, &gpio_init_struct);     /* 初始化LCD_CS引脚 */

    gpio_init_struct.Pin = LCD_WR_GPIO_PIN;
    HAL_GPIO_Init(LCD_WR_GPIO_PORT, &gpio_init_struct);     /* 初始化LCD_WR引脚 */

    gpio_init_struct.Pin = LCD_RD_GPIO_PIN;
    HAL_GPIO_Init(LCD_RD_GPIO_PORT, &gpio_init_struct);     /* 初始化LCD_RD引脚 */

    gpio_init_struct.Pin = LCD_RS_GPIO_PIN;
    HAL_GPIO_Init(LCD_RS_GPIO_PORT, &gpio_init_struct);     /* 初始化LCD_RS引脚 */

    gpio_init_struct.Pin = LCD_BL_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    HAL_GPIO_Init(LCD_BL_GPIO_PORT, &gpio_init_struct);     /* LCD_BL引脚模式设置(推挽输出) */
    
    gpio_init_struct.Pin = LCD_DATA_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    HAL_GPIO_Init(LCD_DATA_GPIO_PORT, &gpio_init_struct);   /* LCD_DATA引脚模式设置 */
    
    LCD_WR(1);                  /* WR 默认高电平 */
    LCD_RD(1);                  /* RD 默认高电平 */
    LCD_CS(1);                  /* CS 默认高电平 */
    LCD_RS(1);                  /* RS 默认高电平 */
    LCD_DATA_OUT(0XFFFF);       /* DATA 默认高电平 */

    lcd_opt_delay(0X1FFFF);

    /* 尝试9341 ID的读取 */
    lcd_wr_regno(0xD3);
    lcddev.id = lcd_rd_data();  /* dummy read */
    lcddev.id = lcd_rd_data();  /* 读到0x00 */
    lcddev.id = lcd_rd_data();  /* 读取93 */
    lcddev.id <<= 8;
    lcddev.id |= lcd_rd_data(); /* 读取41 */

    if (lcddev.id != 0x9341)    /* 不是 9341 , 尝试看看是不是 ST7789 */
    {
        lcd_wr_regno(0x04);
        lcddev.id = lcd_rd_data();      /* dummy read */
        lcddev.id = lcd_rd_data();      /* 读到0x85 */
        lcddev.id = lcd_rd_data();      /* 读取0x85 */
        lcddev.id <<= 8;
        lcddev.id |= lcd_rd_data();     /* 读取0x52 */
        
        if (lcddev.id == 0x8552)        /* 将8552的ID转换成7789 */
        {
            lcddev.id = 0x7789;
        }

        if (lcddev.id != 0x7789)        /* 也不是ST7789, 尝试是不是 NT35310 */
        {
            lcd_wr_regno(0xD4);
            lcddev.id = lcd_rd_data();  /* dummy read */
            lcddev.id = lcd_rd_data();  /* 读回0x01 */
            lcddev.id = lcd_rd_data();  /* 读回0x53 */
            lcddev.id <<= 8;
            lcddev.id |= lcd_rd_data(); /* 这里读回0x10 */

            if (lcddev.id != 0x5310)    /* 也不是NT35310,尝试看看是不是ST7796 */
            {
                lcd_wr_regno(0XD3);
                lcddev.id = lcd_rd_data();  /* dummy read */
                lcddev.id = lcd_rd_data();  /* 读到0X00 */
                lcddev.id = lcd_rd_data();  /* 读取0X77 */
                lcddev.id <<= 8;
                lcddev.id |= lcd_rd_data(); /* 读取0X96 */
                
                if (lcddev.id != 0x7796)    /* 也不是ST7796,尝试看看是不是NT35510 */
                {
                    /* 发送密钥（厂家提供） */
                    lcd_write_reg(0xF000, 0x0055);
                    lcd_write_reg(0xF001, 0x00AA);
                    lcd_write_reg(0xF002, 0x0052);
                    lcd_write_reg(0xF003, 0x0008);
                    lcd_write_reg(0xF004, 0x0001);
                    
                    lcd_wr_regno(0xC500);       /* 读取ID低八位 */
                    lcddev.id = lcd_rd_data();  /* 读回0x80 */
                    lcddev.id <<= 8;

                    lcd_wr_regno(0xC501);       /* 读取ID高八位 */
                    lcddev.id |= lcd_rd_data(); /* 读回0x00 */
                    
                    delay_ms(5);                /* 等待5ms, 因为0XC501指令对1963来说就是软件复位指令, 等待5ms让1963复位完成再操作 */

                    if (lcddev.id != 0x5510)    /* 也不是NT5510,尝试看看是不是ILI9806 */
                    {
                        lcd_wr_regno(0XD3);
                        lcddev.id = lcd_rd_data();  /* dummy read */
                        lcddev.id = lcd_rd_data();  /* 读回0X00 */
                        lcddev.id = lcd_rd_data();  /* 读回0X98 */
                        lcddev.id <<= 8;
                        lcddev.id |= lcd_rd_data(); /* 读回0X06 */
                        
                        if (lcddev.id != 0x9806)    /* 也不是ILI9806,尝试看看是不是SSD1963 */
                        {
                            lcd_wr_regno(0xA1);
                            lcddev.id = lcd_rd_data();
                            lcddev.id = lcd_rd_data();  /* 读回0x57 */
                            lcddev.id <<= 8;
                            lcddev.id |= lcd_rd_data(); /* 读回0x61 */

                            if (lcddev.id == 0x5761) lcddev.id = 0x1963; /* SSD1963读回的ID是5761H,为方便区分,我们强制设置为1963 */
                        }
                    }
                }
            }
        }
    }
    
    if (lcddev.id == 0x7789)
    {
        lcd_ex_st7789_reginit();    /* 执行ST7789初始化 */
    }
    else if (lcddev.id == 0x9341)
    {
        lcd_ex_ili9341_reginit();   /* 执行ILI9341初始化 */
    }
    else if (lcddev.id == 0x5310)
    {
        lcd_ex_nt35310_reginit();   /* 执行NT35310初始化 */
    }
    else if (lcddev.id == 0x7796)
    {
        lcd_ex_st7796_reginit();    /* 执行ST7796初始化 */
    }
    else if (lcddev.id == 0x5510)
    {
        lcd_ex_nt35510_reginit();   /* 执行NT35510初始化 */
    }
    else if (lcddev.id == 0x9806)
    {
        lcd_ex_ili9806_reginit();   /* 执行ILI9806初始化 */
    }
    else if (lcddev.id == 0x1963)
    {
        lcd_ex_ssd1963_reginit();   /* 执行SSD1963初始化 */
        lcd_ssd_backlight_set(100); /* 背光设置为最亮 */
    }

    lcd_display_dir(0); /* 默认为竖屏 */
    
    /* 获取LCD型号信息并打印 */
    const char* lcd_model = "Unknown";
    switch(lcddev.id)
    {
        case 0x7789: lcd_model = "ST7789"; break;
        case 0x9341: lcd_model = "ILI9341"; break;
        case 0x5310: lcd_model = "NT35310"; break;
        case 0x5510: lcd_model = "NT35510"; break;
        case 0x1963: lcd_model = "SSD1963"; break;
        case 0x7796: lcd_model = "ST7796"; break;
        case 0x9806: lcd_model = "ILI9806"; break;
        default: lcd_model = "Unknown"; break;
    }

    /* 特别注意, 如果在main函数里面屏蔽串口1初始化, 则会卡死在printf
     * 里面(卡死在f_putc函数), 所以, 必须初始化串口1, 或者屏蔽掉下面
     * 这行 printf 语句 !!!!!!!
     */
    LCD_LOG_I("LCD: %s, ID: 0x%04X, Resolution: %dx%d", 
           lcd_model, lcddev.id, lcddev.width, lcddev.height);
    
    LCD_BL(1);          /* 点亮背光 */
    lcd_clear(g_back_color);
}

/* -------------------------------------------- 图形绘制函数 ------------------------------------------- */

/**
 * @brief       清除LCD全屏幕内容并填充指定颜色
 * @param       color: 填充颜色值（16位）
 * @retval      无
 * @note        此函数会将整个屏幕填充为指定的颜色
 *              实现原理：调用lcd_fill()函数填充整个屏幕区域
 *              填充范围：(0,0) 到 (lcddev.width-1, lcddev.height-1)
 *              常用于屏幕初始化、清除旧内容或设置背景色
 * @see         lcd_fill() - 实际执行填充操作的函数
 */
void lcd_clear(uint16_t color)
{
    uint32_t index = 0;
    uint32_t totalpoint = lcddev.width;

    totalpoint *= lcddev.height;    /* 得到总点数 */
    lcd_set_cursor(0x00, 0x0000);   /* 设置光标位置 */
    lcd_write_ram_prepare();        /* 开始写入GRAM */

    /* 为了提高写入速度, 将lcd_wr_data函数进行拆分, 避免重复设置
     * RS, CS的操作, 从而提升速度, 从51帧提高到78帧左右, 提高50%
     * 测试条件: -O2优化, 纯刷屏
     * 在有速度要求的时候, 可以继续优化: lcd_fill, lcd_color_fill, 
     * lcd_set_cursor 和 lcd_draw_point 等函数, 大家可以自行优化
     */
    LCD_RS(1);                      /* RS=1,表示写数据 */
    LCD_CS(0);
    
    for (index = 0; index < totalpoint; index++)
    {
        LCD_DATA_OUT(color);        /* 写入要写的数据 */
        LCD_WR(0);
        LCD_WR(1);
    }
    LCD_CS(1);
}

/**
 * @brief       在LCD指定矩形区域内填充单一颜色
 * @param       sx: 填充区域左上角X坐标
 * @param       sy: 填充区域左上角Y坐标
 * @param       ex: 填充区域右下角X坐标
 * @param       ey: 填充区域右下角Y坐标
 * @param       color: 填充颜色值（32位，兼容LTDC，实际使用低16位）
 * @retval      无
 * @note        填充区域大小为: (ex - sx + 1) * (ey - sy + 1) 个像素
 *              实现原理：逐行扫描，每行连续写入像素数据
 *              优化策略：先设置光标到行首，再连续写入整行数据
 *              适用于绘制矩形、清除区域、填充背景等操作
 * @warning     不检查坐标边界，调用前需确保坐标在屏幕范围内
 *              需要确保 sx <= ex 且 sy <= ey
 */
void lcd_fill(uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey, uint32_t color)
{
    uint16_t i, j;
    uint16_t xlen = 0;
    xlen = ex - sx + 1;

    for (i = sy; i <= ey; i++)
    {
        lcd_set_cursor(sx, i);      /* 设置光标位置 */
        lcd_write_ram_prepare();    /* 开始写入GRAM */

        for (j = 0; j < xlen; j++)
        {
            lcd_wr_data(color);     /* 显示颜色 */
        }
    }
}

/**
 * @brief       在LCD指定矩形区域内填充多颜色数据（如图像）
 * @param       sx: 填充区域左上角X坐标
 * @param       sy: 填充区域左上角Y坐标
 * @param       ex: 填充区域右下角X坐标
 * @param       ey: 填充区域右下角Y坐标
 * @param       color: 指向颜色数据数组的指针（16位颜色值数组）
 * @retval      无
 * @note        填充区域大小为: (ex - sx + 1) * (ey - sy + 1) 个像素
 *              颜色数据数组必须包含 width * height 个元素
 *              数据排列方式：逐行扫描，从左到右，从上到下
 *              实现原理：逐行读取颜色数据并写入LCD显存
 *              适用于显示图像、图标、位图等操作
 * @warning     不检查坐标边界和数据有效性，调用前需确保参数正确
 *              color数组必须包含足够的数据，否则会导致内存越界
 */
void lcd_color_fill(uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey, uint16_t *color)
{
    uint16_t height, width;
    uint16_t i, j;

    width = ex - sx + 1;            /* 得到填充的宽度 */
    height = ey - sy + 1;           /* 高度 */

    for (i = 0; i < height; i++)
    {
        lcd_set_cursor(sx, sy + i); /* 设置光标位置 */
        lcd_write_ram_prepare();    /* 开始写入GRAM */

        for (j = 0; j < width; j++)
        {
            lcd_wr_data(color[i * width + j]);  /* 写入数据 */
        }
    }
}

/**
 * @brief       使用Bresenham算法在LCD上绘制直线
 * @param       x1: 直线起点X坐标
 * @param       y1: 直线起点Y坐标
 * @param       x2: 直线终点X坐标
 * @param       y2: 直线终点Y坐标
 * @param       color: 直线颜色值（16位）
 * @retval      无
 * @note        采用Bresenham直线算法，可以高效绘制任意方向的直线
 *              该算法优点：只使用整数运算，速度快，适合嵌入式系统
 *              支持水平、垂直、斜线等各种方向的直线绘制
 *              算法原理：通过计算误差值来决定下一个像素的位置
 * @warning     不检查坐标边界，调用前需确保坐标在屏幕范围内
 */
void lcd_draw_line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color)
{
    uint16_t t;
    int xerr = 0, yerr = 0, delta_x, delta_y, distance;
    int incx, incy, row, col;
    delta_x = x2 - x1;      /* 计算坐标增量 */
    delta_y = y2 - y1;
    row = x1;
    col = y1;

    if (delta_x > 0)
    {
        incx = 1;       /* 设置单步方向 */
    }
    else if (delta_x == 0)
    {
        incx = 0;       /* 垂直线 */
    }
    else
    {
        incx = -1;
        delta_x = -delta_x;
    }

    if (delta_y > 0)
    {
        incy = 1;
    }
    else if (delta_y == 0)
    {
        incy = 0;       /* 水平线 */
    }
    else
    {
        incy = -1;
        delta_y = -delta_y;
    }

    if ( delta_x > delta_y)
    {
        distance = delta_x;  /* 选取基本增量坐标轴 */
    }
    else
    {
        distance = delta_y;
    }

    for (t = 0; t <= distance + 1; t++)     /* 画线输出 */
    {
        lcd_draw_point(row, col, color);    /* 画点 */
        xerr += delta_x;
        yerr += delta_y;

        if (xerr > distance)
        {
            xerr -= distance;
            row += incx;
        }

        if (yerr > distance)
        {
            yerr -= distance;
            col += incy;
        }
    }
}

/**
 * @brief       在LCD上绘制水平直线
 * @param       x: 水平线起点X坐标（左端点）
 * @param       y: 水平线起点Y坐标
 * @param       len: 水平线的长度（像素单位）
 * @param       color: 直线颜色值（16位）
 * @retval      无
 * @note        实现原理：调用lcd_fill()函数填充一个1像素高的矩形区域
 *              绘制范围：从(x,y)到(x+len-1,y)
 *              相比于通用的lcd_draw_line()，此函数专门优化了水平线绘制
 *              常用于绘制表格线、分割线、填充实心圆等操作
 * @warning     不检查坐标边界，调用前需确保线段在屏幕范围内
 *              当len=0或坐标超出边界时，函数会直接返回
 */
void lcd_draw_hline(uint16_t x, uint16_t y, uint16_t len, uint16_t color)
{
    if ((len == 0) || (x > lcddev.width) || (y > lcddev.height))
    {
        return;
    }

    lcd_fill(x, y, x + len - 1, y, color);
}

/**
 * @brief       在LCD上绘制矩形轮廓（空心矩形）
 * @param       x1: 矩形左上角X坐标
 * @param       y1: 矩形左上角Y坐标
 * @param       x2: 矩形右下角X坐标
 * @param       y2: 矩形右下角Y坐标
 * @param       color: 矩形轮廓颜色值（16位）
 * @retval      无
 * @note        实现原理：绘制四条边框线构成矩形轮廓
 *              - 上边：从(x1,y1)到(x2,y1)
 *              - 下边：从(x1,y2)到(x2,y2)
 *              - 左边：从(x1,y1)到(x1,y2)
 *              - 右边：从(x2,y1)到(x2,y2)
 *              只绘制边框，不填充内部，适用于绘制边框、窗口轮廓等
 * @warning     不检查坐标边界，调用前需确保矩形在屏幕范围内
 *              需要确保 x1 <= x2 且 y1 <= y2
 */
void lcd_draw_rectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color)
{
    lcd_draw_line(x1, y1, x2, y1, color);
    lcd_draw_line(x1, y1, x1, y2, color);
    lcd_draw_line(x1, y2, x2, y2, color);
    lcd_draw_line(x2, y1, x2, y2, color);
}

/**
 * @brief       使用Bresenham算法在LCD上绘制圆形轮廓
 * @param       x0: 圆心X坐标
 * @param       y0: 圆心Y坐标
 * @param       r: 圆的半径（像素单位）
 * @param       color: 圆形轮廓颜色值（16位）
 * @retval      无
 * @note        采用Bresenham圆形算法，只绘制圆的轮廓，不填充内部
 *              该算法优点：只使用整数运算，效率高，适合嵌入式系统
 *              算法原理：利用圆的八对称性，只计算八分之一，其余部分通过对称得到
 *              判断条件：di = 3 - 2*r，通过di的正负来决定下一个像素位置
 * @warning     不检查坐标边界，调用前需确保圆形在屏幕范围内
 */
void lcd_draw_circle(uint16_t x0, uint16_t y0, uint8_t r, uint16_t color)
{
    int a, b;
    int di;

    a = 0;
    b = r;
    di = 3 - (r << 1);       /* 判断下个点位置的标志 */

    while (a <= b)
    {
        lcd_draw_point(x0 + a, y0 - b, color);  /* 5 */
        lcd_draw_point(x0 + b, y0 - a, color);  /* 0 */
        lcd_draw_point(x0 + b, y0 + a, color);  /* 4 */
        lcd_draw_point(x0 + a, y0 + b, color);  /* 6 */
        lcd_draw_point(x0 - a, y0 + b, color);  /* 1 */
        lcd_draw_point(x0 - b, y0 + a, color);
        lcd_draw_point(x0 - a, y0 - b, color);  /* 2 */
        lcd_draw_point(x0 - b, y0 - a, color);  /* 7 */
        a++;

        /* 使用Bresenham算法画圆 */
        if (di < 0)
        {
            di += 4 * a + 6;
        }
        else
        {
            di += 10 + 4 * (a - b);
            b--;
        }
    }
}

/**
 * @brief       在LCD上绘制填充的实心圆
 * @param       x: 圆心X坐标
 * @param       y: 圆心Y坐标
 * @param       r: 圆的半径（像素单位）
 * @param       color: 实心圆填充颜色值（16位）
 * @retval      无
 * @note        使用水平线填充算法绘制实心圆
 *              算法原理：通过计算圆周上每个点的位置，然后绘制水平线填充
 *              imax = r * 707 / 1000 + 1：计算需要处理的最大索引（约为45度角）
 *              使用整数运算避免浮点计算，提高效率
 *              通过对称性减少计算量，同时绘制上下两部分
 * @warning     不检查坐标边界，调用前需确保圆形在屏幕范围内
 */
void lcd_fill_circle(uint16_t x, uint16_t y, uint16_t r, uint16_t color)
{
    uint32_t i;
    uint32_t imax = ((uint32_t)r * 707) / 1000 + 1;
    uint32_t sqmax = (uint32_t)r * (uint32_t)r + (uint32_t)r / 2;
    uint32_t xr = r;

    lcd_draw_hline(x - r, y, 2 * r, color);

    for (i = 1; i <= imax; i++)
    {
        if ((i * i + xr * xr) > sqmax)
        {
            /* draw lines from outside */
            if (xr > imax)
            {
                lcd_draw_hline (x - i + 1, y + xr, 2 * (i - 1), color);
                lcd_draw_hline (x - i + 1, y - xr, 2 * (i - 1), color);
            }

            xr--;
        }

        /* draw lines from inside (center) */
        lcd_draw_hline(x - xr, y + i, 2 * xr, color);
        lcd_draw_hline(x - xr, y - i, 2 * xr, color);
    }
}

/* -------------------------------------------- 字符显示函数 ------------------------------------------- */

/**
 * @brief       在LCD指定位置显示ASCII字符
 * @param       x: 字符显示的左上角X坐标
 * @param       y: 字符显示的左上角Y坐标
 * @param       chr: 要显示的ASCII字符（支持范围：' '(0x20) ~ '~'(0x7E)）
 * @param       size: 字体大小，支持 12/16/24/32 像素
 * @param       mode: 显示模式
 *                    0 - 非叠加模式（清除背景，显示背景色）
 *                    1 - 叠加模式（不清除背景，透明显示）
 * @param       color: 字符颜色值（16位）
 * @retval      无
 * @note        字体数据存储在lcdfont.h中，采用点阵字体格式
 *              字符大小和实际像素大小的对应关系：
 *              - 12: 6x12像素  - 16: 8x16像素
 *              - 24: 12x24像素 - 32: 16x32像素
 *              非叠加模式下，字符背景会显示为全局背景色g_back_color
 * @warning     不检查坐标边界，调用前需确保字符在屏幕范围内
 *              不支持中文字符，仅支持ASCII字符
 */
void lcd_show_char(uint16_t x, uint16_t y, char chr, uint8_t size, uint8_t mode, uint16_t color)
{
    uint8_t temp, t1, t;
    uint16_t y0 = y;
    uint8_t csize = 0;
    uint8_t *pfont = 0;

    csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); /* 得到字体一个字符对应点阵集所占的字节数 */
    chr = chr - ' ';    /* 得到偏移后的值（ASCII字库是从空格开始取模，所以-' '就是对应字符的字库） */

    switch (size)
    {
        case 12:
            pfont = (uint8_t *)asc2_1206[chr];  /* 调用1206字体 */
            break;

        case 16:
            pfont = (uint8_t *)asc2_1608[chr];  /* 调用1608字体 */
            break;

        case 24:
            pfont = (uint8_t *)asc2_2412[chr];  /* 调用2412字体 */
            break;

        case 32:
            pfont = (uint8_t *)asc2_3216[chr];  /* 调用3216字体 */
            break;

        default:
            return ;
    }

    for (t = 0; t < csize; t++)
    {
        temp = pfont[t];                            /* 获取字符的点阵数据 */

        for (t1 = 0; t1 < 8; t1++)                  /* 一个字节8个点 */
        {
            if (temp & 0x80)                        /* 有效点,需要显示 */
            {
                lcd_draw_point(x, y, color);        /* 画点出来,要显示这个点 */
            }
            else if (mode == 0)                     /* 无效点,不显示 */
            {
                lcd_draw_point(x, y, g_back_color); /* 画背景色,相当于这个点不显示(注意背景色由全局变量控制) */
            }

            temp <<= 1;                             /* 移位, 以便获取下一个位的状态 */
            y++;

            if (y >= lcddev.height)return;          /* 超区域了 */

            if ((y - y0) == size)                   /* 显示完一列了? */
            {
                y = y0; /* y坐标复位 */
                x++;    /* x坐标递增 */

                if (x >= lcddev.width)
                {
                    return;       /* x坐标超区域了 */
                }

                break;
            }
        }
    }
}

/**
 * @brief       计算整数幂运算（m的n次方）
 * @param       m: 底数（8位无符号整数，范围 0~255）
 * @param       n: 指数（8位无符号整数，范围 0~255）
 * @retval      计算结果 m^n（32位无符号整数）
 * @note        此函数为数字显示函数的辅助函数
 *              使用简单的循环乘法实现幂运算，避免使用浮点运算
 *              主要用于计算十进制数字的各位数值
 *              例如：lcd_pow(10, 2) = 100，用于提取百位数
 * @warning     此函数为静态函数，仅供本文件内部使用
 *              不检查溢出，大数值计算可能导致结果溢出
 */
static uint32_t lcd_pow(uint8_t m, uint8_t n)
{
    uint32_t result = 1;

    while (n--)
    {
        result *= m;
    }

    return result;
}

/**
 * @brief       在LCD上显示指定位数的数字（去除前导零）
 * @param       x: 数字显示的起始X坐标（左上角）
 * @param       y: 数字显示的起始Y坐标（左上角）
 * @param       num: 要显示的数值（0 ~ 4294967295，即 2^32-1）
 * @param       len: 显示的数字位数（包括前导零）
 * @param       size: 字体大小，支持 12/16/24/32 像素
 * @param       color: 数字颜色值（16位）
 * @retval      无
 * @note        此函数会自动去除前导零，但保留最后一位
 *              例如：num=123, len=5 显示为 "  123"（前面用空格填充）
 *              例如：num=0, len=3 显示为 "  0"（保留最后一位0）
 *              数字间距 = size/2 像素，总宽度 = len * size/2 像素
 *              适用于显示变量数值、计数器等场景
 * @warning     不检查坐标边界，调用前需确保显示区域在屏幕范围内
 *              当num的位数超过len时，可能会显示不完整
 */
void lcd_show_num(uint16_t x, uint16_t y, uint32_t num, uint8_t len, uint8_t size, uint16_t color)
{
    uint8_t t, temp;
    uint8_t enshow = 0;

    for (t = 0; t < len; t++)   /* 按总显示位数循环 */
    {
        temp = (num / lcd_pow(10, len - t - 1)) % 10;   /* 获取对应位的数字 */

        if (enshow == 0 && t < (len - 1))               /* 没有使能显示,且还有位要显示 */
        {
            if (temp == 0)
            {
                lcd_show_char(x + (size / 2) * t, y, ' ', size, 0, color);  /* 显示空格,占位 */
                continue;       /* 继续下个一位 */
            }
            else
            {
                enshow = 1;     /* 使能显示 */
            }
        }

        lcd_show_char(x + (size / 2) * t, y, temp + '0', size, 0, color);   /* 显示字符 */
    }
}

/**
 * @brief       在LCD上显示指定位数的数字（可选择保留前导零）
 * @param       x: 数字显示的起始X坐标（左上角）
 * @param       y: 数字显示的起始Y坐标（左上角）
 * @param       num: 要显示的数值（0 ~ 4294967295，即 2^32-1）
 * @param       len: 显示的数字位数（固定位数）
 * @param       size: 字体大小，支持 12/16/24/32 像素
 * @param       mode: 显示模式控制字（8位）
 *                    位[7]: 0-不填充前导零（用空格），1-填充前导零
 *                    位[6:1]: 保留位（未使用）
 *                    位[0]: 0-非叠加显示（清除背景），1-叠加显示（透明）
 * @param       color: 数字颜色值（16位）
 * @retval      无
 * @note        相比于lcd_show_num()，此函数可以控制是否显示前导零
 *              例如：num=123, len=5, mode=0x80 显示为 "00123"
 *              例如：num=123, len=5, mode=0x00 显示为 "  123"
 *              数字间距 = size/2 像素，总宽度 = len * size/2 像素
 *              适用于显示时间、日期、固定格式数字等场景
 * @warning     不检查坐标边界，调用前需确保显示区域在屏幕范围内
 *              当num的位数超过len时，可能会显示不完整
 */
void lcd_show_xnum(uint16_t x, uint16_t y, uint32_t num, uint8_t len, uint8_t size, uint8_t mode, uint16_t color)
{
    uint8_t t, temp;
    uint8_t enshow = 0;

    for (t = 0; t < len; t++)       /* 按总显示位数循环 */
    {
        temp = (num / lcd_pow(10, len - t - 1)) % 10;    /* 获取对应位的数字 */

        if (enshow == 0 && t < (len - 1))   /* 没有使能显示,且还有位要显示 */
        {
            if (temp == 0)
            {
                if (mode & 0x80)    /* 高位需要填充0 */
                {
                    lcd_show_char(x + (size / 2) * t, y, '0', size, mode & 0x01, color);    /* 用0占位 */
                }
                else
                {
                    lcd_show_char(x + (size / 2) * t, y, ' ', size, mode & 0x01, color);    /* 用空格占位 */
                }

                continue;
            }
            else
            {
                enshow = 1;         /* 使能显示 */
            }

        }

        lcd_show_char(x + (size / 2) * t, y, temp + '0', size, mode & 0x01, color);
    }
}

/**
 * @brief       在LCD指定区域内显示ASCII字符串（支持自动换行）
 * @param       x: 显示区域的左上角X坐标
 * @param       y: 显示区域的左上角Y坐标
 * @param       width: 显示区域的宽度（像素单位）
 * @param       height: 显示区域的高度（像素单位）
 * @param       size: 字体大小，支持 12/16/24/32 像素
 * @param       p: 指向要显示的字符串的指针（以\0结尾）
 * @param       color: 字符串颜色值（16位）
 * @retval      无
 * @note        支持的ASCII字符范围：' '(0x20) ~ '~'(0x7E)
 *              自动换行机制：当字符超出宽度边界时，自动换到下一行
 *              自动截断机制：当字符超出高度边界时，停止显示
 *              字符间距 = size/2 像素，行间距 = size 像素
 *              使用非叠加模式显示，背景会显示为全局背景色
 *              适用于显示文本信息、菜单项、状态信息等
 * @warning     不检查坐标边界和指针有效性，调用前需确保参数正确
 *              不支持中文字符，仅支持ASCII字符
 *              字符串指针p不能为NULL，否则会导致程序崩溃
 */
void lcd_show_string(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t size, char *p, uint16_t color)
{
    uint8_t x0 = x;
    
    width += x;
    height += y;

    while ((*p <= '~') && (*p >= ' '))   /* 判断是不是非法字符! */
    {
        if (x >= width)
        {
            x = x0;
            y += size;
        }

        if (y >= height)
        {
            break;      /* 退出 */
        }

        lcd_show_char(x, y, *p, size, 0, color);
        x += size / 2;
        p++;
    }
}
